import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';

const DEFAULT_FIELDS_FILE = path.join(
  process.cwd(),
  'src/components/editor/schema/fields/aiComponents.ts',
);
const DEFAULT_EDIT_FILE = path.join(
  process.cwd(),
  'src/components/editor/schema/edit/aiComponents.ts',
);
const DEFAULT_PREVIEW_ROOT = path.join(
  process.cwd(),
  'src/components/editor/schema/preview/components',
);
const DEFAULT_RIGHT_ROOT = path.join(process.cwd(), 'src/components/editor/Right');
const REQUIRED_MODULE_PATH = '@/components/editor/schema/preview/aiCustomComponents';
const BELONG_TO_DIR_OVERRIDES: Record<string, string> = {
  staticTextStyle: 'renderStaticTexStyle',
  renderTrendDataConfig: 'renderTrendDataConfig',
};
const OPTIONAL_FIELDS_FILE = path.join(
  process.cwd(),
  '.claude/agents/ChainValidator/config/optional-fields.json',
);
const PANEL_KNOWLEDGE_FILE = path.join(
  process.cwd(),
  'Workflow/FigmaMCPWorkflow/Resources/knowledge-base/panel-reuse-patterns.json',
);
const LAYOUT_PLACEHOLDER_KEYS = new Set(['slot']);
const OPTIONAL_FIELD_KEYS = new Set<string>();
const GLOBAL_FIELD_KEYS = new Set(['visible', 'groupSetting', 'publishTime', 'expireTime']);
const UNIVERSAL_PANELS = new Set(['header', 'eventReportSetting', 'groupSetting', 'publishInfo']);
const PANEL_FILE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
const optionalFieldConfig = loadOptionalFieldConfig();
(optionalFieldConfig.optionalFields || []).forEach((key) => OPTIONAL_FIELD_KEYS.add(key));
(optionalFieldConfig.globalFields || []).forEach((key) => GLOBAL_FIELD_KEYS.add(key));
const PANEL_HINTS = loadPanelKnowledge();

export interface ChainValidatorOptions {
  componentDir?: string;
  fieldsFile?: string;
  editFile?: string;
  previewFile?: string;
  previewDir?: string;
  rightRoot?: string;
}

export class ChainValidator {
  private errors: ValidationError[] = [];
  private warnings: ValidationWarning[] = [];
  private fieldsData: Record<string, string> = {};
  private editData: Array<{ key: string; belongTo: string }> = [];
  private previewContent = '';
  private previewPath = '';
  private fieldModule?: string;
  private rightRoot = DEFAULT_RIGHT_ROOT;
  private rightIndexPath = path.join(DEFAULT_RIGHT_ROOT, 'index.tsx');
  private rightPanelAliasMap = new Map<string, string[]>();
  private fieldUsage: Record<
    string,
    {
      isGlobal: boolean;
      inForm: boolean;
      inPanel: boolean;
      belongTo: Set<string>;
      usedInPreview: boolean;
      hasDefault: boolean;
    }
  > = {};
  private panelFieldCache = new Map<string, Set<string>>();
  private panelStats = { total: 0, handled: 0 };
  private panelOnlyFields = new Set<string>();

  async validateChain(
    componentType: string,
    optionsOrDir?: string | ChainValidatorOptions,
  ): Promise<ValidationResult> {
    const options: ChainValidatorOptions =
      typeof optionsOrDir === 'string' ? { componentDir: optionsOrDir } : optionsOrDir || {};

    this.errors = [];
    this.warnings = [];
    this.panelOnlyFields.clear();

    try {
      this.panelFieldCache.clear();
      this.panelStats = { total: 0, handled: 0 };
      await this.loadArtifacts(componentType, options);
      this.validateFieldNode();
      this.validateEditFields();
      this.validatePanelMappings(componentType);
      this.validatePreview();
      this.validateAIIsolation(componentType);

      const fieldsCoverage = this.calculateFieldsCoverage();
      const editCoverage = this.calculateEditCoverage();
      const previewCoverage = this.calculatePreviewCoverage();
      const editMappingCoverage = this.calculateEditMappingCoverage();

      this.generateFieldWarnings();

      return {
        status: this.errors.length === 0 ? 'pass' : 'fail',
        valid: this.errors.length === 0,
        errors: this.errors,
        warnings: this.warnings,
        fields_coverage: fieldsCoverage,
        edit_coverage: editCoverage,
        preview_coverage: previewCoverage,
        edit_mapping_coverage: editMappingCoverage,
        panel_only_fields: Array.from(this.panelOnlyFields),
        score: this.calculateChainScore(
          fieldsCoverage,
          editCoverage,
          previewCoverage,
          editMappingCoverage,
        ),
        connectivity: this.calculateConnectivity(),
      };
    } catch (error: any) {
      this.errors.push({
        level: 'error',
        layer: 'system',
        field: 'unknown',
        message: `验证失败: ${error.message}`,
      });

      return {
        status: 'fail',
        valid: false,
        errors: this.errors,
        warnings: this.warnings,
        fields_coverage: '0%',
        edit_coverage: '0%',
        preview_coverage: '0%',
        edit_mapping_coverage: '0%',
        panel_only_fields: Array.from(this.panelOnlyFields),
        score: 0,
        connectivity: 0,
      };
    }
  }

  private async loadArtifacts(componentType: string, options: ChainValidatorOptions) {
    const fieldsPath = this.resolveExisting([
      options.fieldsFile,
      options.componentDir && path.join(options.componentDir, 'config/fields.generated.ts'),
      DEFAULT_FIELDS_FILE,
    ]);
    if (!fieldsPath) {
      throw new Error('无法找到 FieldNode 配置文件');
    }
    const fieldInfo = this.extractFieldInfo(fieldsPath, componentType);
    this.fieldsData = fieldInfo.props;
    this.fieldUsage = {};
    Object.keys(this.fieldsData).forEach((key) => {
      this.fieldUsage[key] = {
        isGlobal: GLOBAL_FIELD_KEYS.has(key),
        inForm: false,
        inPanel: false,
        belongTo: new Set(),
        usedInPreview: false,
        hasDefault: this.hasDefaultValue(this.fieldsData[key]),
      };
    });
    this.fieldModule = fieldInfo.module;

    const editPath = this.resolveExisting([
      options.editFile,
      options.componentDir && path.join(options.componentDir, 'config/edit.generated.ts'),
      DEFAULT_EDIT_FILE,
    ]);
    if (!editPath) {
      throw new Error('无法找到 EditFields 文件');
    }
    this.editData = this.extractEditInfo(editPath, componentType);

    const previewPath = this.resolveExisting([
      options.previewFile,
      options.previewDir && path.join(options.previewDir, `${componentType}.tsx`),
      options.componentDir && path.join(options.componentDir, 'preview', 'index.tsx'),
      path.join(DEFAULT_PREVIEW_ROOT, componentType, 'index.tsx'),
    ]);
    if (!previewPath) {
      throw new Error('无法找到 Preview 组件文件');
    }
    this.previewPath = previewPath;
    this.previewContent = fs.readFileSync(previewPath, 'utf8');

    this.rightRoot = options.rightRoot ? path.resolve(options.rightRoot) : DEFAULT_RIGHT_ROOT;
    this.rightIndexPath = path.join(this.rightRoot, 'index.tsx');
    this.buildRightPanelAliasMap();
  }

  private resolveExisting(candidates: Array<string | undefined>): string | null {
    for (const candidate of candidates) {
      if (!candidate) continue;
      const resolved = path.resolve(candidate);
      if (fs.existsSync(resolved)) {
        return resolved;
      }
    }
    return null;
  }

  private extractFieldInfo(filePath: string, componentType: string): FieldInfo {
    const source = ts.createSourceFile(
      filePath,
      fs.readFileSync(filePath, 'utf8'),
      ts.ScriptTarget.ESNext,
      true,
      ts.ScriptKind.TS,
    );

    const result: FieldInfo = { props: {} };

    const visit = (node: ts.Node) => {
      if (ts.isObjectLiteralExpression(node)) {
        let typeMatch = false;
        let moduleValue: string | undefined;
        const props: Record<string, string> = {};

        node.properties.forEach((prop) => {
          if (!ts.isPropertyAssignment(prop)) return;
          const name = this.getPropName(prop.name);
          if (!name) return;

          if (name === 'type' && ts.isStringLiteral(prop.initializer)) {
            typeMatch = prop.initializer.text === componentType;
          } else if (name === 'module' && ts.isStringLiteral(prop.initializer)) {
            moduleValue = prop.initializer.text;
          } else if (name === 'props' && ts.isObjectLiteralExpression(prop.initializer)) {
            prop.initializer.properties.forEach((child) => {
              if (ts.isPropertyAssignment(child)) {
                const childName = this.getPropName(child.name);
                if (childName) {
                  props[childName] = child.initializer.getText().trim();
                }
              }
            });
          }
        });

        if (typeMatch) {
          result.module = moduleValue;
          result.props = props;
          return;
        }
      }
      ts.forEachChild(node, visit);
    };

    ts.forEachChild(source, visit);

    if (!Object.keys(result.props).length) {
      throw new Error(`未在 ${filePath} 中找到 ${componentType} 的 FieldNode 配置`);
    }

    return result;
  }

  private extractEditInfo(filePath: string, componentType: string) {
    const source = ts.createSourceFile(
      filePath,
      fs.readFileSync(filePath, 'utf8'),
      ts.ScriptTarget.ESNext,
      true,
      ts.ScriptKind.TS,
    );

    const items: Array<{ key: string; belongTo: string }> = [];

    const visit = (node: ts.Node) => {
      if (ts.isPropertyAssignment(node)) {
        const name = this.getPropName(node.name);
        if (name === componentType && ts.isArrayLiteralExpression(node.initializer)) {
          node.initializer.elements.forEach((element) => {
            if (ts.isObjectLiteralExpression(element)) {
              let key = '';
              let belongTo = '';

              element.properties.forEach((prop) => {
                if (!ts.isPropertyAssignment(prop)) return;
                const propName = this.getPropName(prop.name);
                if (!propName) return;

                if (propName === 'key' && ts.isStringLiteral(prop.initializer)) {
                  key = prop.initializer.text;
                }
                if (propName === 'belongTo' && ts.isStringLiteral(prop.initializer)) {
                  belongTo = prop.initializer.text;
                }
              });

              if (key) {
                items.push({ key, belongTo });
              }
            }
          });
        }
      }
      ts.forEachChild(node, visit);
    };

    ts.forEachChild(source, visit);

    if (!items.length) {
      throw new Error(`未在 ${filePath} 中找到 ${componentType} 的 EditFields 配置`);
    }

    return items;
  }

  private getPropName(name?: ts.PropertyName): string | undefined {
    if (!name) return undefined;
    if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
      return name.text.toString();
    }
    return undefined;
  }

  private validateFieldNode() {
    Object.entries(this.fieldsData).forEach(([key, value]) => {
      if (value === 'undefined' || value === '') {
        this.errors.push({
          level: 'error',
          layer: 'fields',
          field: key,
          message: `FieldNode prop "${key}" 缺少默认值`,
        });
      }
    });
  }

  private validateEditFields() {
    const fieldsKeys = new Set(Object.keys(this.fieldsData));

    this.editData.forEach((item) => {
      if (!item.key || LAYOUT_PLACEHOLDER_KEYS.has(item.key)) return;
      if (!fieldsKeys.has(item.key)) {
        this.errors.push({
          level: 'error',
          layer: 'edit',
          field: item.key,
          message: `EditFields 引用了不存在的 Field "${item.key}"`,
        });
        return;
      }

      const usage = this.fieldUsage[item.key];
      if (usage) {
        usage.inForm = true;
        if (item.belongTo) {
          usage.belongTo.add(item.belongTo);
        }
      }

      if (!item.belongTo || item.belongTo === 'unknown') {
        this.warnings.push({
          level: 'warning',
          layer: 'edit',
          field: item.key,
          message: 'belongTo 未指定，无法确定面板归属',
        });
      }
    });
  }

  private validatePanelMappings(componentType: string) {
    const processedPanels = new Set<string>();

    this.editData.forEach((item) => {
      const belongTo = item.belongTo;
      if (!belongTo || processedPanels.has(belongTo)) {
        return;
      }

      processedPanels.add(belongTo);
      const isUniversal = UNIVERSAL_PANELS.has(belongTo);
      if (!isUniversal) {
        this.panelStats.total += 1;
      }

      const resolution = this.resolvePanelInfo(belongTo, componentType, isUniversal);
      if (!resolution.exists) {
        if (!isUniversal) {
          this.errors.push({
            level: 'error',
            layer: 'edit',
            field: belongTo,
            message: `面板 ${belongTo} 缺少实现目录`,
          });
        }
        return;
      }

      if (!isUniversal && !resolution.handled) {
        this.errors.push({
          level: 'error',
          layer: 'edit',
          field: belongTo,
          message: `渲染面板 ${belongTo} 未注册 ${componentType}（${resolution.reason}）`,
        });
      } else {
        if (!isUniversal) {
          this.panelStats.handled += 1;
        }
        this.applyPanelFieldUsage(belongTo, componentType, resolution);
      }
    });
  }

  private resolvePanelInfo(
    belongTo: string,
    componentType: string,
    isUniversal: boolean,
  ): PanelResolution {
    const dirName = this.getPanelDirName(belongTo);
    const dirPath = path.join(this.rightRoot, dirName);

    if (!fs.existsSync(dirPath)) {
      const aliasResolution = this.resolvePanelAlias(belongTo, componentType);
      if (aliasResolution) {
        return aliasResolution;
      }
      if (isUniversal) {
        return { exists: false, handled: true, files: [], indexPath: null };
      }
      return { exists: false, handled: false, reason: '目录缺失', files: [], indexPath: null };
    }

    const indexCandidates = [path.join(dirPath, 'index.tsx'), path.join(dirPath, 'index.ts')];
    const indexPath = indexCandidates.find((file) => fs.existsSync(file)) || null;

    const files = new Set<string>();
    let handled = false;

    this.findDirectPanelFiles(dirPath, componentType).forEach((file) => {
      handled = true;
      files.add(file);
    });

    if (indexPath) {
      this.extractPanelFilesFromIndex(indexPath, componentType, dirPath).forEach((file) => {
        handled = true;
        files.add(file);
      });

      if (!handled && this.fileMentionsComponent(indexPath, componentType)) {
        handled = true;
        files.add(indexPath);
      }
    }

    if (!handled && isUniversal && indexPath) {
      handled = true;
      files.add(indexPath);
    }

    if (!handled) {
      const aliasResolution = this.resolvePanelAlias(belongTo, componentType);
      if (aliasResolution) {
        aliasResolution.files.forEach((file) => files.add(file));
        handled = files.size > 0;
        if (handled) {
          return {
            exists: true,
            handled: true,
            reason: '',
            files: Array.from(files),
            indexPath,
          };
        }
      }
    }

    return {
      exists: true,
      handled,
      reason: handled ? '' : '未找到组件引用',
      files: Array.from(files),
      indexPath,
    };
  }

  private getPanelDirName(belongTo: string) {
    if (!belongTo) return '';
    if (BELONG_TO_DIR_OVERRIDES[belongTo]) {
      return BELONG_TO_DIR_OVERRIDES[belongTo];
    }
    if (/^render[A-Z]/.test(belongTo)) {
      return belongTo;
    }
    return `render${this.toPascalCase(belongTo)}`;
  }

  private fileMentionsComponent(filePath: string, componentType: string) {
    const content = fs.readFileSync(filePath, 'utf8');
    return content.includes(`'${componentType}'`) || content.includes(`"${componentType}"`);
  }

  private findDirectPanelFiles(dirPath: string, componentType: string) {
    const files: string[] = [];
    const candidates = [`render${componentType}`, componentType];

    candidates.forEach((base) => {
      PANEL_FILE_EXTENSIONS.forEach((ext) => {
        const filePath = path.join(dirPath, `${base}${ext}`);
        if (fs.existsSync(filePath)) {
          files.push(filePath);
        }
      });
    });

    return files;
  }

  private extractPanelFilesFromIndex(indexPath: string, componentType: string, dirPath: string) {
    const source = ts.createSourceFile(
      indexPath,
      fs.readFileSync(indexPath, 'utf8'),
      ts.ScriptTarget.ESNext,
      true,
      ts.ScriptKind.TSX,
    );

    const importMap = this.collectImportMap(source, dirPath);
    const identifiers = new Set<string>();

    const visit = (node: ts.Node) => {
      if (ts.isIfStatement(node)) {
        const conditionText = node.expression.getText();
        if (
          conditionText.includes(`'${componentType}'`) ||
          conditionText.includes(`"${componentType}"`)
        ) {
          this.collectReturnIdentifiers(node.thenStatement).forEach((id) => identifiers.add(id));
        }
      } else if (ts.isSwitchStatement(node)) {
        node.caseBlock.clauses.forEach((clause) => {
          if (
            ts.isCaseClause(clause) &&
            clause.expression.getText().replace(/["']/g, '') === componentType
          ) {
            clause.statements.forEach((statement) => {
              this.collectReturnIdentifiers(statement).forEach((id) => identifiers.add(id));
            });
          }
        });
      }

      ts.forEachChild(node, visit);
    };

    ts.forEachChild(source, visit);

    const files = new Set<string>();
    identifiers.forEach((identifier) => {
      const target = importMap.get(identifier);
      if (!target) return;
      const resolved = this.resolveImportModulePath(target, dirPath);
      if (resolved) {
        files.add(resolved);
      }
    });

    return Array.from(files);
  }

  private collectImportMap(source: ts.SourceFile, dirPath: string) {
    const map = new Map<string, string>();

    source.statements.forEach((statement) => {
      if (!ts.isImportDeclaration(statement) || !statement.importClause) return;
      if (!ts.isStringLiteral(statement.moduleSpecifier)) return;

      const modulePath = statement.moduleSpecifier.text;
      const localPath = path.resolve(dirPath, modulePath);

      if (statement.importClause.name) {
        map.set(statement.importClause.name.text, localPath);
      }

      const bindings = statement.importClause.namedBindings;
      if (bindings && ts.isNamedImports(bindings)) {
        bindings.elements.forEach((element) => {
          map.set(element.name.text, localPath);
        });
      } else if (bindings && ts.isNamespaceImport(bindings)) {
        map.set(bindings.name.text, localPath);
      }
    });

    return map;
  }

  private collectReturnIdentifiers(statement: ts.Statement | ts.Node): string[] {
    const identifiers: string[] = [];

    const visit = (node: ts.Node) => {
      if (ts.isReturnStatement(node) && node.expression && ts.isCallExpression(node.expression)) {
        const callee = node.expression.expression;
        if (ts.isIdentifier(callee)) {
          identifiers.push(callee.text);
        }
      }
      ts.forEachChild(node, visit);
    };

    visit(statement);
    return identifiers;
  }

  private resolveImportModulePath(modulePath: string, dirPath: string) {
    const direct = this.resolveWithExtensions(modulePath);
    if (direct) return direct;
    const relative = this.resolveWithExtensions(path.resolve(dirPath, modulePath));
    return relative;
  }

  private resolveWithExtensions(basePath: string) {
    if (fs.existsSync(basePath)) {
      const stat = fs.statSync(basePath);
      if (stat.isFile()) {
        return basePath;
      }
    }
    for (const ext of PANEL_FILE_EXTENSIONS) {
      const candidate = `${basePath}${ext}`;
      if (fs.existsSync(candidate)) {
        return candidate;
      }
    }
    for (const ext of PANEL_FILE_EXTENSIONS) {
      const candidate = path.join(basePath, `index${ext}`);
      if (fs.existsSync(candidate)) {
        return candidate;
      }
    }
    return null;
  }

  private applyPanelFieldUsage(
    belongTo: string,
    componentType: string,
    resolution: PanelResolution,
  ) {
    const cacheKey = this.getPanelCacheKey(belongTo, componentType);
    if (this.panelFieldCache.has(cacheKey)) {
      this.markFieldsFromPanel(this.panelFieldCache.get(cacheKey)!);
      return;
    }

    const filesToScan = resolution.files.length
      ? resolution.files
      : resolution.indexPath
        ? [resolution.indexPath]
        : [];

    const collected = new Set<string>();
    const aliasMap = this.collectHandleChangeAliases(filesToScan);
    filesToScan.forEach((file) => {
      this.extractHandleChangeKeys(file, aliasMap.get(file) || new Map()).forEach((key) =>
        collected.add(key),
      );
    });

    this.panelFieldCache.set(cacheKey, collected);
    this.markFieldsFromPanel(collected);
  }

  private markFieldsFromPanel(keys: Set<string>) {
    keys.forEach((key) => {
      const usage = this.fieldUsage[key];
      if (usage) {
        usage.inPanel = true;
      }
    });
  }

  private buildRightPanelAliasMap() {
    this.rightPanelAliasMap.clear();
    if (!fs.existsSync(this.rightIndexPath)) {
      return;
    }

    const indexDir = path.dirname(this.rightIndexPath);
    const source = ts.createSourceFile(
      this.rightIndexPath,
      fs.readFileSync(this.rightIndexPath, 'utf8'),
      ts.ScriptTarget.ESNext,
      true,
      ts.ScriptKind.TSX,
    );
    const importMap = this.collectImportMap(source, indexDir);

    const visit = (node: ts.Node) => {
      if (ts.isPropertyAssignment(node)) {
        const panelKey = this.getPropName(node.name);
        if (!panelKey || !ts.isObjectLiteralExpression(node.initializer)) {
          ts.forEachChild(node, visit);
          return;
        }

        const renderProp = node.initializer.properties.find(
          (prop): prop is ts.PropertyAssignment =>
            ts.isPropertyAssignment(prop) && this.getPropName(prop.name) === 'render',
        );

        if (renderProp) {
          const files = this.extractFilesFromRender(renderProp.initializer, importMap, indexDir);
          if (files.length) {
            this.rightPanelAliasMap.set(panelKey, files);
          }
        }
      }
      ts.forEachChild(node, visit);
    };

    visit(source);
  }

  private resolvePanelAlias(belongTo: string, componentType: string): PanelResolution | null {
    const baseFiles = this.rightPanelAliasMap.get(belongTo);
    if (!baseFiles || !baseFiles.length) {
      return null;
    }
    const files = new Set<string>();
    baseFiles.forEach((file) => {
      if (file) {
        files.add(file);
        if (this.isIndexFile(file)) {
          this.extractPanelFilesFromIndex(file, componentType, path.dirname(file)).forEach((child) =>
            files.add(child),
          );
        }
      }
    });
    if (!files.size) {
      return null;
    }
    return {
      exists: true,
      handled: true,
      files: Array.from(files),
      indexPath: null,
    };
  }

  private extractFilesFromRender(
    expression: ts.Expression,
    importMap: Map<string, string>,
    baseDir: string,
  ) {
    const files = new Set<string>();
    const inspect = (node: ts.Node) => {
      if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
        if (ts.isBlock(node.body)) {
          node.body.statements.forEach((statement) => inspect(statement));
        } else {
          inspect(node.body);
        }
        return;
      }

      if (ts.isReturnStatement(node) && node.expression) {
        inspect(node.expression);
        return;
      }

      if (ts.isCallExpression(node)) {
        const resolved = this.resolveCallExpressionTarget(node, importMap, baseDir);
        if (resolved) {
          files.add(resolved);
        }
      }
      ts.forEachChild(node, inspect);
    };

    inspect(expression);
    return Array.from(files);
  }

  private resolveCallExpressionTarget(
    node: ts.CallExpression,
    importMap: Map<string, string>,
    baseDir: string,
  ) {
    const callee = node.expression;

    if (ts.isIdentifier(callee)) {
      const modulePath = importMap.get(callee.text);
      if (modulePath) {
        return this.resolveWithExtensions(modulePath);
      }
      return null;
    }

    if (ts.isPropertyAccessExpression(callee)) {
      if (ts.isIdentifier(callee.expression)) {
        const modulePath = importMap.get(callee.expression.text);
        if (modulePath) {
          return this.resolveWithExtensions(modulePath);
        }
      } else if (ts.isCallExpression(callee.expression)) {
        const innerCall = callee.expression;
        if (
          ts.isIdentifier(innerCall.expression) &&
          innerCall.expression.text === 'require' &&
          innerCall.arguments.length &&
          ts.isStringLiteral(innerCall.arguments[0])
        ) {
          const modulePath = path.resolve(baseDir, innerCall.arguments[0].text);
          return this.resolveWithExtensions(modulePath);
        }
      }
    }

    return null;
  }

  private isIndexFile(filePath: string) {
    const base = path.basename(filePath);
    return /^index\.(t|j)sx?$/.test(base);
  }

  private collectHandleChangeAliases(files: string[]) {
    const aliasMap = new Map<string, Map<string, number[]>>();
    files.forEach((file) => {
      const source = ts.createSourceFile(
        file,
        fs.readFileSync(file, 'utf8'),
        ts.ScriptTarget.ESNext,
        true,
        ts.ScriptKind.TSX,
      );
      const map = this.scanHandleChangeAliases(source);
      aliasMap.set(file, map);
    });
    return aliasMap;
  }

  private extractHandleChangeKeys(filePath: string, aliasMap: Map<string, number[]>) {
    const content = fs.readFileSync(filePath, 'utf8');
    const source = ts.createSourceFile(
      filePath,
      content,
      ts.ScriptTarget.ESNext,
      true,
      ts.ScriptKind.TSX,
    );

    const keys = new Set<string>();

    const visit = (node: ts.Node) => {
      if (ts.isCallExpression(node)) {
        const callee = node.expression;
        if (this.isHandleChangeExpression(callee)) {
          node.arguments.forEach((arg) => this.collectStringLiterals(arg, keys));
        } else if (ts.isIdentifier(callee) && aliasMap.has(callee.text)) {
          const indexes = aliasMap.get(callee.text) || [];
          indexes.forEach((idx) => {
            const arg = node.arguments[idx];
            if (arg) {
              this.collectStringLiterals(arg, keys);
            }
          });
        }
      }
      ts.forEachChild(node, visit);
    };

    ts.forEachChild(source, visit);
    return keys;
  }

  private scanHandleChangeAliases(source: ts.SourceFile) {
    const map = new Map<string, number[]>();

    const visit = (node: ts.Node) => {
      if (ts.isVariableStatement(node)) {
        node.declarationList.declarations.forEach((decl) => {
          if (!decl.name || !ts.isIdentifier(decl.name) || !decl.initializer) return;
          const alias = this.extractAliasInfo(decl.initializer);
          if (alias.length) {
            map.set(decl.name.text, alias);
          }
        });
      } else if (ts.isFunctionDeclaration(node) && node.name) {
        const alias = this.extractAliasInfo(node);
        if (alias.length) {
          map.set(node.name.text, alias);
        }
      }
      ts.forEachChild(node, visit);
    };

    visit(source);
    return map;
  }

  private extractAliasInfo(node: ts.Node) {
    if (!this.isFunctionLike(node)) {
      return [];
    }
    const params = node.parameters || [];
    const paramIndexMap = new Map<string, number>();
    params.forEach((param, index) => {
      if (ts.isIdentifier(param.name)) {
        paramIndexMap.set(param.name.text, index);
      }
    });
    if (!paramIndexMap.size) return [];

    const indices = new Set<number>();
    const visit = (inner: ts.Node) => {
      if (ts.isCallExpression(inner) && this.isHandleChangeExpression(inner.expression)) {
        inner.arguments.forEach((arg) => {
          if (ts.isIdentifier(arg) && paramIndexMap.has(arg.text)) {
            indices.add(paramIndexMap.get(arg.text)!);
          }
        });
      }
      ts.forEachChild(inner, visit);
    };

    if (node.body) {
      visit(node.body);
    }

    return Array.from(indices);
  }

  private isFunctionLike(node: ts.Node): node is ts.FunctionLikeDeclarationBase & {
    body?: ts.ConciseBody;
  } {
    return (
      ts.isArrowFunction(node) ||
      ts.isFunctionExpression(node) ||
      ts.isFunctionDeclaration(node)
    );
  }

  private isHandleChangeExpression(expression: ts.LeftHandSideExpression) {
    if (ts.isIdentifier(expression)) {
      return expression.text === 'handleChange';
    }
    if (ts.isPropertyAccessExpression(expression)) {
      return expression.expression.getText() === 'handleChange';
    }
    if (ts.isElementAccessExpression(expression)) {
      return expression.expression.getText() === 'handleChange';
    }
    return expression.getText().startsWith('handleChange');
  }

  private collectStringLiterals(node: ts.Node, bucket: Set<string>) {
    if (!node) return;
    if (ts.isStringLiteralLike(node)) {
      bucket.add(node.text);
      return;
    }
    if (ts.isBinaryExpression(node)) {
      this.collectStringLiterals(node.left, bucket);
      this.collectStringLiterals(node.right, bucket);
      return;
    }
    if (ts.isConditionalExpression(node)) {
      this.collectStringLiterals(node.whenTrue, bucket);
      this.collectStringLiterals(node.whenFalse, bucket);
      return;
    }
    if (ts.isParenthesizedExpression(node)) {
      this.collectStringLiterals(node.expression, bucket);
      return;
    }
    if (ts.isArrayLiteralExpression(node)) {
      node.elements.forEach((element) => this.collectStringLiterals(element, bucket));
      return;
    }
    if (ts.isCallExpression(node) || ts.isPropertyAccessExpression(node)) {
      node.forEachChild((child) => this.collectStringLiterals(child, bucket));
    }
  }

  private getPanelCacheKey(belongTo: string, componentType: string) {
    return `${belongTo}::${componentType}`;
  }

  private toPascalCase(value: string) {
    if (!value) return '';
    return value.charAt(0).toUpperCase() + value.slice(1);
  }

  private validatePreview() {
    Object.keys(this.fieldsData).forEach((key) => {
      if (key === 'visible') return;
      const usagePattern = new RegExp(`(data|props|\\{|\\s)\\.?${key}\\b`);
      const used = usagePattern.test(this.previewContent);
      if (this.fieldUsage[key]) {
        this.fieldUsage[key].usedInPreview = used;
      }
    });
  }

  private validateAIIsolation(type: string) {
    if (this.fieldModule && this.fieldModule !== REQUIRED_MODULE_PATH) {
      this.errors.push({
        level: 'error',
        layer: 'fields',
        field: 'module',
        message: `module 必须指向 ${REQUIRED_MODULE_PATH}，当前为: ${this.fieldModule}`,
      });
    }

    if (this.previewPath) {
      const normalized = this.previewPath.replace(/\\/g, '/');
      const expected = `/preview/components/${type}/`;
      if (!normalized.includes(expected)) {
        this.warnings.push({
          level: 'warning',
          layer: 'preview',
          field: 'path',
          message: `Preview 路径应位于 ${expected} 下，当前为: ${this.previewPath}`,
        });
      }
    }
  }

  private calculateFieldsCoverage(): string {
    const targetKeys = this.getTargetFieldKeys();
    if (!targetKeys.length) return '100%';
    const covered = targetKeys.filter((key) => this.fieldUsage[key].hasDefault).length;
    return `${Math.round((covered / targetKeys.length) * 100)}%`;
  }

  private calculateEditCoverage(): string {
    const targetKeys = this.getTargetFieldKeys();
    if (!targetKeys.length) return '100%';
    const covered = targetKeys.filter((key) => {
      const usage = this.fieldUsage[key];
      return usage.inForm || usage.inPanel;
    }).length;
    return `${Math.round((covered / targetKeys.length) * 100)}%`;
  }

  private calculatePreviewCoverage(): string {
    const targetKeys = this.getTargetFieldKeys();
    if (!targetKeys.length) return '100%';
    const used = targetKeys.filter((key) => this.fieldUsage[key].usedInPreview).length;
    return `${Math.round((used / targetKeys.length) * 100)}%`;
  }

  private calculateEditMappingCoverage(): string {
    if (!this.panelStats.total) return '100%';
    return `${Math.round((this.panelStats.handled / this.panelStats.total) * 100)}%`;
  }

  private getTargetFieldKeys() {
    return Object.keys(this.fieldUsage).filter(
      (key) => key !== 'visible' && !this.fieldUsage[key].isGlobal && !OPTIONAL_FIELD_KEYS.has(key),
    );
  }

  private hasDefaultValue(value?: string) {
    if (typeof value !== 'string') return false;
    const trimmed = value.trim();
    if (!trimmed) return false;
    if (trimmed === 'undefined') return false;
    return true;
  }

  private calculateConnectivity(): number {
    let score = 100;
    score -= this.errors.length * 10;
    score -= this.warnings.length * 2;
    return Math.max(0, score);
  }

  private calculateChainScore(
    fieldsCoverage: string,
    editCoverage: string,
    previewCoverage: string,
    editMappingCoverage: string,
  ) {
    const coverages = [fieldsCoverage, editCoverage, previewCoverage, editMappingCoverage].map((value) =>
      this.parsePercent(value),
    );
    const weights = [0.2, 0.2, 0.4, 0.2];
    const weighted = coverages.reduce((sum, value, idx) => sum + value * weights[idx], 0);
    const penalty = this.errors.length * 10 + this.warnings.length * 2;
    const rawScore = weighted - penalty;
    return Math.max(0, Math.min(100, Math.round(rawScore)));
  }

  private parsePercent(value: string): number {
    const match = value.match(/(\d+)/);
    if (!match) return 0;
    return Math.max(0, Math.min(100, parseInt(match[1], 10)));
  }

  private generateFieldWarnings() {
    Object.entries(this.fieldUsage).forEach(([key, usage]) => {
      if (key === 'visible' || usage.isGlobal || OPTIONAL_FIELD_KEYS.has(key)) return;
      if (!(usage.inForm || usage.inPanel)) {
        const hints = this.getPanelSuggestions(key);
        const hintText = hints.length ? `（推荐面板：${hints.join(', ')}）` : '';
        this.warnings.push({
          level: 'warning',
          layer: 'edit',
          field: key,
          message: `Field "${key}" 未在 Edit 配置或自定义面板中暴露${hintText}`,
        });
      }
      if (!usage.usedInPreview) {
        const exposed = usage.inForm || usage.inPanel;
        this.warnings.push({
          level: 'warning',
          layer: 'preview',
          field: key,
          message: exposed
            ? `Field "${key}" 已在 Edit/面板中暴露，但 Preview 未使用`
            : `Preview 未使用 field "${key}"`,
        });
        if (exposed) {
          this.panelOnlyFields.add(key);
        }
      }
    });
  }

  private getPanelSuggestions(field: string) {
    const hints = PANEL_HINTS.get(field);
    return hints ? Array.from(hints) : [];
  }
}

interface PanelResolution {
  exists: boolean;
  handled: boolean;
  reason?: string;
  files: string[];
  indexPath?: string | null;
}

interface FieldInfo {
  module?: string;
  props: Record<string, string>;
}

interface ValidationError {
  level: 'error';
  layer: string;
  field: string;
  message: string;
}

interface ValidationWarning {
  level: 'warning';
  layer: string;
  field: string;
  message: string;
}

interface ValidationResult {
  status: 'pass' | 'fail';
  valid: boolean;
  errors: ValidationError[];
  warnings: ValidationWarning[];
  fields_coverage: string;
  edit_coverage: string;
  preview_coverage: string;
  edit_mapping_coverage: string;
  panel_only_fields: string[];
  score: number;
  connectivity: number;
}

interface OptionalFieldConfig {
  optionalFields?: string[];
  globalFields?: string[];
}

type PanelHintMap = Map<string, Set<string>>;

function loadOptionalFieldConfig(): OptionalFieldConfig {
  try {
    const raw = fs.readFileSync(OPTIONAL_FIELDS_FILE, 'utf8');
    const parsed = JSON.parse(raw);
    return {
      optionalFields: Array.isArray(parsed?.optionalFields) ? parsed.optionalFields : [],
      globalFields: Array.isArray(parsed?.globalFields) ? parsed.globalFields : [],
    };
  } catch (error) {
    return { optionalFields: [], globalFields: [] };
  }
}

function loadPanelKnowledge(): PanelHintMap {
  const map: PanelHintMap = new Map();
  try {
    const raw = fs.readFileSync(PANEL_KNOWLEDGE_FILE, 'utf8');
    const parsed = JSON.parse(raw);
    const patterns = parsed?.panelReusePatterns || {};
    Object.entries(patterns).forEach(([panelName, detail]: [string, any]) => {
      const supported: string[] = Array.isArray(detail?.supportedFields) ? detail.supportedFields : [];
      supported.forEach((field) => {
        const normalized = (field || '').toString().trim();
        if (!normalized) return;
        if (!map.has(normalized)) {
          map.set(normalized, new Set());
        }
        map.get(normalized)!.add(panelName);
      });
    });
  } catch (error) {
    // ignore missing knowledge file
  }
  return map;
}
